package com.bolinfest.closure;

import java.text.Collator;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import com.google.common.base.Join;
import com.google.common.base.Preconditions;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.collect.TreeMultimap;
import com.google.javascript.jscomp.AbstractCompiler;
import com.google.javascript.jscomp.CompilerPass;
import com.google.javascript.jscomp.NodeTraversal;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.JSDocInfo.Visibility;

class ExportPublicMembersPass implements CompilerPass, NodeTraversal.Callback {

  private final AbstractCompiler compiler;

  private final Multimap<String, String> exportedProperties;
  
  private final Multimap<String, String> enums;

  private final Set<String> externs;

  ExportPublicMembersPass(AbstractCompiler compiler) {
    this.compiler = compiler;
    this.exportedProperties = TreeMultimap.create(Collator.getInstance(),
        Collator.getInstance());
    this.enums = LinkedHashMultimap.create();
    this.externs = Sets.newLinkedHashSet("superClass_");
  }

  @Override
  public void process(Node externs, Node root) {
    NodeTraversal.traverse(compiler, root, this);
  }

  @Override
  public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n,
      Node parent) {
    // Traverse all nodes.
    return true;
  }

  @Override
  public void visit(NodeTraversal t, final Node node, Node parent) {
    if (node.getType() == Token.ASSIGN) {
      JSDocInfo info = node.getJSDocInfo();
      if (info != null) {
        Visibility visibility = info.getVisibility();
        switch (visibility) {
        case PUBLIC:
        case INHERITED:
        case PROTECTED:
          addExports(info, node, visibility == Visibility.PROTECTED);
          break;
        case PRIVATE:
          return;
        default:
          throw new RuntimeException("Unknown Visibility: " + visibility);
        }
      }
    }
  }

  private void addExports(JSDocInfo info, Node node, boolean isExtern) {
    LinkedList<String> names = Lists.newLinkedList();
    Node getPropNode = node.getFirstChild();
    // TODO(bolinfest): Ignore if the right side is goog.typedef.
    do {
      Node firstChild = getPropNode.getFirstChild();
      if (firstChild.getType() == Token.NAME) {
        names.addFirst(firstChild.getNext().getString());
        names.addFirst(firstChild.getString());
        break;
      } else if (firstChild.getType() == Token.GETPROP) {
        getPropNode = firstChild;
        names.addFirst(getPropNode.getNext().getString());
      } else {
        // TODO(bolinfest): Grok this state.
        // throw new IllegalStateException("Ambiguous export: " +
        //     t.getInput() + " line: " + t.getLineNumber());
        return;
      }
    } while (getPropNode != null);

    String fullyQualifiedName = Join.join(".", names);
    if (fullyQualifiedName.startsWith("goog.debug")) {
      // goog.debug will be stripped by DebugStrippingCompilerRunner, so there
      // is no need to export anything from the goog.debug namespace.
      return;
    } else if ("goog.base".equals(fullyQualifiedName)) {
      // goog.base is a Compiler primitive that should not be exported.
      return;
    }

    if (isExtern) {
      externs.add(names.getLast());
    } if (info.hasEnumParameterType()) {
      Node firstChild = node.getFirstChild();
      while (firstChild.getType() != Token.NAME) {
        firstChild = firstChild.getFirstChild();
      }
      quoteEnumKeys(fullyQualifiedName, node.getFirstChild().getNext());
    } else {
      int lastIndex = fullyQualifiedName.lastIndexOf(".");
      exportedProperties.put(fullyQualifiedName.substring(0, lastIndex),
          fullyQualifiedName.substring(lastIndex + 1));
    }    
  }

  String getExternsJs() {
    StringBuilder builder = new StringBuilder();
    for (String extern : externs) {
      builder.append("Object.prototype." + extern + ";\n");
    }
    return builder.toString();
  }
  
  private void quoteEnumKeys(String enumName, Node objectliteral) {
    Preconditions.checkState(objectliteral.getType() == Token.OBJECTLIT,
        "Expected object literal but found: %s", objectliteral);
    for (Node node = objectliteral.getFirstChild(); node != null;
        node = node.getNext().getNext()) {
      Preconditions.checkState(node.getType() == Token.STRING,
          "Expected a string but found: %s", node);
        enums.put(enumName, node.getString());
    }
  }

  String getExportsJs() {
    StringBuilder builder = new StringBuilder();
    for (final String export : exportedProperties.keySet()) {
      builder.append("goog.exportSymbol('" + export + "', " +
          export + ");\n");
      List<String> properties = Lists.newLinkedList();
      boolean hasIteratorProperty = false;
      for (String property : exportedProperties.get(export)) {
        if ("__iterator__".equals(property)) {
          hasIteratorProperty = true;
          continue;
        }
        properties.add("\"" + property + "\": " + export + "." + property);
      }
      builder.append("goog.object.extend(" + export + ", {" +
          Join.join(",", properties) + "});\n");
      if (hasIteratorProperty) {
        builder.append(export + "['__iterator__'] = " + export +
            ".__iterator__;\n");
      }
    }

    addEnumExports(builder);
    return builder.toString();
  }

  private void addEnumExports(StringBuilder builder) {
    for (String enumName : enums.keySet()) {
      builder.append("goog.exportSymbol('" + enumName + "', {");
      Collection<String> values = enums.get(enumName);
      List<String> pairs = Lists.newArrayListWithCapacity(values.size());
      for (String value : values) {
        pairs.add("'" + value + "': " + enumName + "." + value);
      }
      builder.append(Join.join(", ", pairs));
      builder.append("}");
      builder.append(");\n");
    }
  }
}
